/**
 * Copyright @2016-present, Sumscope, Inc.
 * All rights reserved.
 */

const Store = require('./store');
const logger = require('../../util/logger');
const config = require('../../config');
const uuid = require('node-uuid');

const defaultCookie = {
  httpOnly: true,
  path: '/',
  maxAge: 24 * 60 * 60 * 1000 //one day in ms
};

module.exports = (opts = {}) => {
  opts = Object.assign({prefix: 'sess'}, opts);
  let cookie = Object.assign({}, defaultCookie, opts.cookie);
  let key = opts.key || config.server.name + '.sid';
  let store;
  if (opts.redis) {
    opts.client = require('../../util/redis').origin();
  }
  if (opts.redisOpt || opts.client) {
    const RedisStore = require('./redis-store');
    store = new RedisStore(opts);
  } else {
    store = new Store(opts);
  }
  
  function generateSession() {
    let session = {};
    session.cookie = {};
    for (let prop in cookie) {
      session.cookie[prop] = cookie[prop];
    }
    return session;
  }
  
  async function getSession() {
    if (!this.sessionId) {
      this.sessionId = this.cookies.get(key, cookie);
    }
    
    let session;
    let isNew = false;
    if (!this.sessionId) {
      logger.debug('session id not exist, generate a new one');
      session = generateSession();
      this.sessionId = uuid.v1();
      isNew = true;
    } else {
      try {
        session = await store.get(this.sessionId);
        logger.debug('get session %j with key %s', session, this.sessionId);
      } catch (err) {
        if (err.code === 'ENOENT') {
          logger.debug('get session error, code = ENOENT');
        } else {
          logger.debug('get session error: ', err.message);
        }
      }
    }
    
    if (!session) {
      logger.debug('session is empty or invalid');
      session = generateSession();
      this.sessionId = uuid.v1();
      this.cookies.set(key, null);
      isNew = true;
    }
    
    return {
      origin: JSON.stringify(session),
      session,
      isNew
    };
  }
  
  async function refreshSession(session, origin, isNew) {
    // reject any session changes, and do not update session expiry
    if (this._sessionSave === false) {
      return logger.debug('session save disabled');
    }
    
    //delete session
    if (!session) {
      if (!isNew) {
        logger.debug('session set to null, destroy session: %s', this.sessionId);
        this.cookies.set(key, null);
        await store.destroy(this.sessionId);
        return this.cookies.set(key, this.sessionId, session.cookie);
      }
      return logger.debug('a new session and set to null, ignore destroy');
    }
    
    // force saving non-empty session
    if (this._sessionSave === true) {
      logger.debug('session save forced');
      return await store.set(this.sessionId, session);
    }
    
    if (!isNew && JSON.stringify(session) === origin) {
      return logger.debug('session is not modified');
    }
    
    logger.debug('session is modified');
    await store.set(this.sessionId, session);
    this.cookies.set(key, this.sessionId, session.cookie);
  }
  
  return async (ctx, next) => {
    if (ctx.session || ctx._session) {
      return await next();
    }
    
    let result = await getSession.call(ctx);
    if (!result) {
      return await next();
    }
    
    ctx.session = result.session;
    let firstError = null;
    try {
      await next();
    } catch (err) {
      logger.debug('next logic error: %s', err.message);
      firstError = err;
    }
    
    try {
      logger.debug('refresh session');
      await refreshSession.call(ctx, ctx.session, result.origin, result.isNew);
    } catch (err) {
      logger.debug('refresh session error: %s', err.message);
      if (firstError) ctx.app.emit('error', err, ctx);
      firstError = firstError || err;
    }
    if (firstError) throw firstError;
  }
};

module.exports.Store = Store;
